<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
	<head>

		<!--
			Copyright (c) 2013-2020 Antoine Martin <antoine@xpra.org>
			Copyright (c) 2014 Joshua Higgins <josh@kxes.net>
			Licensed under MPL 2.0
 		-->

		<title>xpra websockets client</title>
		<meta charset="utf-8">
		<meta name="description" content="xpra websockets client">
		<meta name="apple-mobile-web-app-capable" content="yes">
		<meta name="mobile-web-app-capable" content="yes">
		<link rel="shortcut icon" type="image/png" href="./favicon.png" id="favicon">

		<link rel="stylesheet" href="css/client.css">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<link rel="stylesheet" href="css/spinner.css">

		<script type="text/javascript" src="js/lib/es6-shim.js"></script>
		<script type="text/javascript" src="js/lib/AudioContextMonkeyPatch.js"></script>

		<script type="text/javascript" src="js/lib/jquery.js"></script>
		<script type="text/javascript" src="js/lib/jquery-ui.js"></script>
		<script type="text/javascript" src="js/lib/jquery.ba-throttle-debounce.js"></script>
		<script type="text/javascript" src="js/lib/jquery-transform-draggable.js"></script>

		<script type="text/javascript" src="js/lib/rencode.js"></script>
		<script type="text/javascript" src="js/lib/bencode.js"></script>
		<script type="text/javascript" src="js/lib/zlib.js"></script>
		<script type="text/javascript" src="js/lib/lz4.js"></script>
		<script type="text/javascript" src="js/lib/brotli_decode.js"></script>
		<script type="text/javascript" src="js/lib/forge.js"></script>

		<script type="text/javascript" src="js/lib/jsmpeg.js"></script>
		<script type="text/javascript" src="js/lib/broadway/Decoder.js"></script>
		<script type="text/javascript" src="js/lib/aurora/aurora.js"></script>
		<!--
		<script type="text/javascript" src="js/lib/aurora/mp3.js"></script>
		<script type="text/javascript" src="js/lib/aurora/flac.js"></script>
		<script type="text/javascript" src="js/lib/aurora/aac.js"></script>
		-->
		<script type="text/javascript" src="js/lib/aurora/aurora-xpra.js"></script>

		<script type="text/javascript" src="js/lib/FileSaver.js"></script>
		<script type="text/javascript" src="js/lib/jszip.js"></script>
		<script type="text/javascript" src="js/lib/detect-zoom.js"></script>

		<script type="text/javascript" src="js/Keycodes.js"></script>
		<script type="text/javascript" src="js/Protocol.js"></script>
		<script type="text/javascript" src="js/Window.js"></script>
		<script type="text/javascript" src="js/Notifications.js"></script>
		<script type="text/javascript" src="js/Utilities.js"></script>
		<script type="text/javascript" src="js/MediaSourceUtil.js"></script>
		<script type="text/javascript" src="js/Client.js"></script>

		<link rel="stylesheet" type="text/css" href="css/menu.css"/>
		<script type="application/javascript" src="js/Menu.js"></script>
		<script type="application/javascript" src="js/Menu-custom.js"></script>
		<link rel="stylesheet" type="text/css" href="css/menu-skin.css"/>
		<link rel="stylesheet" type="text/css" href="css/icon.css"/>

		<!-- simple-keyboard -->
		<script type="application/javascript" src="js/lib/simple-keyboard.js"></script>
		<link rel="stylesheet" href="css/simple-keyboard.css">
	</head>

	<body>
		<div id="dpi" style="width: 1in; height: 1in; left: -100%; top: -100%; position: absolute;">
		</div>

		<div id="progress" class="overlay" style="display: none">
			<p id="progress-label"></p>
			<p id="progress-details"></p>
			<progress id="progress-bar" max="100" value="10"></progress>
		</div>

		<div id="screen" style="width: 100%; height:100%;">
			<div id="float_menu">
				<ul class="Menu -horizontal">
					<li class="-hasSubmenu -noChevron">
						<a href="#" title="Xpra" class="noDrag" data-icon="menu"></a>
						<ul id="menu_list">
							<li  class="-hasSubmenu" >
								<a data-icon="apps" href="#">Start</a>
								<ul id="startmenu">
								</ul>
							</li>
							<li class="-hasSubmenu">
								<a href="#" data-icon="kitchen">Server</a>
								<ul>
									<li id="clock_menu_entry"><a href="#" data-icon="access_time" onclick="return false" id="clock_menu_text"></a></li>
									<li><a href="#" data-icon="cloud_upload" onclick="upload_file(event); return false">Upload file</a></li>
									<li><a href="#" data-icon="exit_to_app" onclick="confirm_shutdown_server(event); return false">Shutdown Server</a></li>
								</ul>
							</li>
							<!--
							<li class="-hasSubmenu">
								<a href="#" data-icon="list">Features</a>
								<ul>
									<li><a href="#" data-icon="message">Notifications</a></li>
								</ul>
							</li>
							-->
							<li class="-hasSubmenu">
								<a href="#" data-icon="info">Information</a>
								<ul>
									<li><a href="#" data-icon="info" onclick="show_about(event); return false">About Xpra</a></li>
									<li><a href="#" data-icon="trending_up" onclick="show_sessioninfo(event); return false">Session Info</a></li>
									<li><a href="#" data-icon="bug_report" onclick="show_bugreport(event); return false">Bug Report</a></li>
								</ul>
							</li>
							<li><a href="#" data-icon="exit_to_app" onclick="client.disconnect_reason='User request'; client.close(); return false">Disconnect</a></li>
						</ul>
					</li>
					<li class="-hasSubmenu -noChevron">
						<a href="#" title="Open Windows" data-icon="filter"></a>
						<ul id="open_windows_list">
						</ul>
					</li>
					<li class="-hasSubmenu -noChevron">
						<a href="#" id="fullscreen_button" title="Fullscreen" data-icon="fullscreen"></a>
					</li>
					<li class="-hasSubmenu -noChevron">
						<a href="#" id="keyboard_button" title="Keyboard" data-icon="keyboard"></a>
					</li>
					<li class="-hasSubmenu -noChevron">
						<a href="#" id="sound_button" title="Audio" data-icon="volume_off"></a>
					</li>
				</ul>
				<div id="float_tray"  class="menu-divright"></div>
			</div>
			<img id="shadow_pointer">
		</div>

		<div class="notifications">
		</div>

		<form id="upload_form">
			<input type="file" id="upload" style="display: none" />
		</form>

		<div id="about">
			<h2>Xpra HTML5 Client</h2>
			<h3>Version 4.3</h3>
			<span>
				Copyright (c) 2013-2020 Antoine Martin &lt;antoine@xpra.org&gt;
				<br />
				Copyright (c) 2014 Joshua Higgins &lt;josh@kxes.net&gt;
			</span>
			<br />
			<br />
			<span class="info">
			This code is available under the <a href="./LICENSE">Mozilla Public License Version 2.0</a>
			<br />
			For more information, see <a href="https://github.com/Xpra-org/xpra-html5">xpra HTML5 project</a>.
			</span>
		</div>

		<div id="sessioninfo">
			<h2>Session Information</h2>
			<h3>Connection Data</h3>
			<table id='sessiondata'>
			<tr>
				<th>Server Endpoint</th>
				<td id="endpoint"></td>
			</tr>
			<tr>
				<th>Server Display</th>
				<td id="server_display"></td>
			</tr>
			<!-- <tr>
				<th>Server Hostname</th>
				<td id="server_hostname"></td>
			</tr> -->
			<tr>
				<th>Server Platform</th>
				<td id="server_platform"></td>
			</tr>
			<tr>
				<th>Server Load</th>
				<td id="server_load"></td>
			</tr>
			<!-- <tr>
				<th>Session Started</th>
				<td id="session_started"></td>
			</tr> -->
			<tr>
				<th>Session Connected</th>
				<td id="session_connected"></td>
			</tr>
			<tr>
				<th>Server Latency</th>
				<td id="server_latency"></td>
			</tr>
			<tr>
				<th>Client Latency</th>
				<td id="client_latency"></td>
			</tr>
			</table>
		</div>

		<div id="bugreport">
			<h2>Xpra Bug Report</h2>
			<br />
			<form>
				Summary: <input id="bugreport_summary" type="text" size="64" maxlength="128" value="" />
				<br />
				Description:
				<br />
				<textarea id="bugreport_description" cols="80" rows="6"></textarea>
				<br />
				<button id="bugreport_cancel" type="button" onclick="hide_bugreport()">Cancel</button>
				<button id="bugreport_submit" type="button" onclick="generate_bugreport()" disabled>Generate Report</button>
			</form>
		</div>

		<div>
			<textarea id="pasteboard" style="display: block; position: absolute; left: -99em;"></textarea>
		</div>

		<div class="simple-keyboard" style="display: block; position: fixed; bottom: 0; width: 100%">
		</div>

		<script>
			const default_settings = {};
			const getparam = function(prop) {
				let v = Utilities.getparam(prop);
				if (v==null && prop in default_settings) {
					v = default_settings[prop];
				}
				return v;
			};
			const getboolparam = function(prop, default_value) {
				let v = Utilities.getparam(prop);
				if (v==null && prop in default_settings) {
					v = default_settings[prop];
				}
				if (v===null) {
					return default_value;
				}
				return ["true", "on", "1", "yes", "enabled"].indexOf(String(v).toLowerCase())!==-1;
			};
			const getintparam = function(prop, default_value) {
				let v = Utilities.getparam(prop);
				if (v==null && prop in default_settings) {
					v = default_settings[prop];
				}
				try {
					v = parseInt(v)
				}
				catch(e) {
					return default_value;
				}
				if (v===null || isNaN(v)) {
					return default_value;
				}
				return v;
			}
			const getfloatparam = function(prop, default_value) {
				let v = Utilities.getparam(prop);
				if (v==null && prop in default_settings) {
					v = default_settings[prop];
				}
				try {
					v = parseFloat(v)
				}
				catch(e) {
					return default_value;
				}
				if (v===null || isNaN(v)) {
					return default_value;
				}
				return v;
			}

			const float_menu_item_size = 30;
			const float_menu_padding = 20;
			let float_menu_width = (float_menu_item_size*5) + float_menu_padding;

			let cdebug = function () {
				Utilities.clog.apply(Utilities, arguments);
			};
			let clog = function () {
				Utilities.clog.apply(Utilities, arguments);
			};

			//start calculating the fps asap:
			let animation_times = [];
			let fps = -1;
			let vrefresh = -1;
			function animation_cb(now) {
			    let l = animation_times.push(now);
			    if (l > 100) {
					animation_times.shift();
				}
				let t0 = animation_times[0];
				if (now>t0) {
			        fps = Math.floor(1000 * (l-1) / (now - t0));
				}
				cdebug("draw", "animation_cb", now, "fps", fps, "vrefresh", vrefresh);
				if (vrefresh<0) {
				    window.requestAnimationFrame(animation_cb);
				}
			};
			window.requestAnimationFrame(animation_cb);


			function confirm_shutdown_server() {
				if (confirm("Shutdown this server?")) {
					client.reconnect = false;
					client.send(["shutdown-server"]);
				}
			}

			function upload_file(event) {
				$('.menu-content').slideUp();
				$('#upload').click();
			}

			function cleanup_dialogs() {
				$('.menu-content').slideUp();
				$('#about').hide();
				hide_sessioninfo();
				hide_bugreport();
			}
			function connection_established() {
				//this may be a re-connection,
				//so make sure we cleanup any left-overs
				clog("connection-established");
				cleanup_dialogs();
				$("button.menu-button").prop('disabled', false);
			}
			function connection_lost() {
				clog("connection-lost");
				cleanup_dialogs();
				$("button.menu-button").prop('disabled', true);
			}
			document.addEventListener('connection-established', connection_established);
			document.addEventListener('connection-lost', connection_lost);
			
			$("body").click(function() {
				$('.menu-content').slideUp();
				$('#about').hide();
				hide_sessioninfo();
			});

			$("#pasteboard").blur(function() {
				//force focus back to our capture widget:
				if (client.capture_keyboard) {
					$("#pasteboard").focus();
				}
			});

			function show_about(event) {
				$('.menu-content').slideUp();
				$('#about').show();
				hide_sessioninfo();
				event.stopPropagation();
			}

			//session-info handling:
			function show_sessioninfo(event) {
				$('.menu-content').slideUp();
				$('#about').hide();
				$('#sessiondata td').html("");
				$('#sessioninfo').show();
				client.start_info_timer();
				event.stopPropagation();
			}
			function hide_sessioninfo() {
				$('#sessioninfo').hide();
				client.stop_info_timer();
			}
			document.addEventListener('info-response', function (e) {
				const info = e.data;
				$("#endpoint").html(client.uri);
				$("#server_display").html(client.server_display);
				//$("#server_hostname").html(info["hostname"] || "");
				$("#server_platform").html(client.server_platform);
				if (client.server_load==null) {
					$("#server_load").html("n/a");
				}
				else {
					$("#server_load").html(""+client.server_load);
				}
				function toHHMMSS(t) {
					let s = "";
					const hh = Math.floor(t/1000/60/60);
					if (hh>0)
						s += ""+hh+":";
					const mm = Math.floor(t/1000/60) % 60;
					s += ("0" + mm).slice(-2);	//left pad with "0"
					const ss = Math.floor(t/1000) % 60;
					s += ":"+("0" + ss).slice(-2);	//left pad with "0"
					return s
				}
				//this one shows bogus values!?:
				//var elapsed = (new Date()).getTime()-info["server.start_time"];
				//$("#session_started").html(toHHMMSS(elapsed));
				const elapsed = (new Date()).getTime()-client.client_start_time;
				$("#session_connected").html(toHHMMSS(elapsed));
				if (client.server_ping_latency>=0)
					$("#server_latency").html(client.server_ping_latency);
				if (client.client_ping_latency>=0)
					$("#client_latency").html(client.client_ping_latency);
				//TODO:
				// * add packet counter to Protocol.js
	  			//"Packets Received"
	  			//"Encoding + Compression"
				}, false);


			function enable_bugreport_submit() {
				$("#bugreport_submit").prop('disabled', false);
				document.removeEventListener('info-response', enable_bugreport_submit);
			}
			function hide_bugreport() {
				$('#bugreport').hide();
				//focus back on our capture widget:
				const pasteboard_element = $("#pasteboard");
				pasteboard_element.prop('autofocus');
				pasteboard_element.focus();
			}
			function show_bugreport(event) {
				//stop capturing input, so we can interact with the bugreport form:
				client.capture_keyboard = false;
				$("#pasteboard").removeAttr('autofocus');
				//hide menu:
				$('.menu-content').slideUp();
				$('#bugreport').show();
				event.stopPropagation();
				//disable submit until we get an info-response:
				$("#bugreport_submit").prop('disabled', true);
				document.addEventListener('info-response', enable_bugreport_submit);
				client.send_info_request();
			}
			function generate_bugreport() {
				client.capture_keyboard = true;
				const zip = new JSZip();
				zip.file("Summary.txt", ""+$("#bugreport_summary").val()+"\n\n"+$("#bugreport_description").val());
				zip.file("Server-Info.txt", ""+JSON.stringify(client.server_last_info));
				//screenshot:
				const canvas = document.createElement('canvas');
				canvas.width = client.desktop_width;
				canvas.height = client.desktop_height;
				const ctx = canvas.getContext('2d');
				for (let i in client.id_to_window) {
					const iwin = client.id_to_window[i];
					ctx.drawImage(iwin.draw_canvas, iwin.x, iwin.y);
				}
				const png_base64 = canvas.toDataURL("image/png");
				zip.file("screenshot.png", Utilities.convertDataURIToBinary(png_base64));
				zip.generateAsync({type:"blob"})
				.then(function(content) {
					saveAs(content, "bugreport.zip");
				});
				hide_bugreport();
			}

			let touchaction_scroll = true;

			function toggle_touchaction() {
				touchaction_scroll = !touchaction_scroll;
				set_touchaction();
			}
			function set_touchaction() {
				let touchaction, label;
				if (touchaction_scroll) {
					touchaction = "none";
					label = "zoom";
				}
				else {
					touchaction = "auto";
					label = "scroll";
				}
				cdebug("mouse", "set_touchaction() touchaction=", touchaction, "label=", label);
				$('div.window canvas').css("touch-action", touchaction);
				$('#touchaction_link').html("Set Touch Action: "+label);
				if (!Utilities.isEdge()) {
					$('#touchaction_link').hide();
				}
			}

			function init_client() {
				if (typeof jQuery == 'undefined') {
					window.alert("Incomplete Xpra HTML5 client installation: jQuery is missing, cannot continue.");
					return;
				}
				const https = document.location.protocol=="https:";

				// look at url parameters
				const username = getparam("username") || null;
				const password = getparam("password") || null;
				const sound = getboolparam("sound", true) || null;
				const audio_codec = getparam("audio_codec") || null;
				const encoding = getparam("encoding") || null;
				const bandwidth_limit = getintparam("bandwidth_limit", 0) || 0;
				const action = getparam("action") || "connect";
				const display = getparam("display") || "";
				const shadow_display = getparam("shadow_display") || "";
				const submit = getboolparam("submit", true);
				const server = getparam("server") || window.location.hostname;
				const port = getparam("port") || window.location.port;
				const ssl = getboolparam("ssl", https);
				const path = getparam("path") || window.location.pathname;
				const encryption = getparam("encryption") || null;
				const key = getparam("key") || null;
				const keyboard_layout = getparam("keyboard_layout") || null;
				const start = getparam("start");
				const exit_with_children = getboolparam("exit_with_children", false);
				const exit_with_client = getboolparam("exit_with_client", false);
				const sharing = getboolparam("sharing", false);
				const video = getboolparam("video", Utilities.is_64bit());
				const mediasource_video = getboolparam("mediasource_video", false);
				const remote_logging = getboolparam("remote_logging", true);
				const insecure = getboolparam("insecure", false);
				const ignore_audio_blacklist = getboolparam("ignore_audio_blacklist", false);
				const keyboard = getboolparam("keyboard", Utilities.isMobile());
				const clipboard = getboolparam("clipboard", true);
				const printing = getboolparam("printing", true);
				const file_transfer = getboolparam("file_transfer", true);
				const steal = getboolparam("steal", true);
				const reconnect = getboolparam("reconnect", true);
				const swap_keys = getboolparam("swap_keys", Utilities.isMacOS());
				const scroll_reverse_x = getboolparam("scroll_reverse_x", false);
				const scroll_reverse_y = getboolparam("scroll_reverse_y", Utilities.isMacOS());
				const floating_menu = getboolparam("floating_menu", true);
				const toolbar_position = getparam("toolbar_position");
				const autohide = getboolparam("autohide", false);
				const override_width = getfloatparam("override_width", window.innerWidth);
				vrefresh = getintparam("vrefresh", -1);
				if (vrefresh<0 && fps>=30) {
					vrefresh = fps;
				}
				
				try {
					sessionStorage.removeItem("password");
				}
				catch (e) {
					//ignore
				}

				var scale = 1;
				if (window.innerWidth!==override_width) {
					scale = override_width/window.innerWidth;
				}

				let progress_timer;
				const progress_value = 0;
				let progress_offset = 0;
				// show connection progress:
				function connection_progress(state, details, progress) {
					clog("connection_progress(", state, ", ", details, ", ", progress, ")");
					if (progress>=100) {
						$('#progress').hide();
					}
					else {
						$('#progress').show();
					}
					$('#progress-label').text(state || " ");
					$('#progress-details').text(details || " ");
					$('#progress-bar').val(progress);
					const progress_value = progress;
					if (progress_timer) {
						window.clearTimeout(progress_timer);
						progress_timer = null;
					}
					if (progress<100) {
						progress_move_offset();
					}
				}
				// the offset is just to make the user feel better
				// nothing changes, but we just show a slow progress
				function progress_move_offset() {
					progress_timer = null;
					progress_offset++;
					$('#progress-bar').val(progress_value + progress_offset);
					if (progress_offset<9) {
						progress_timer = window.setTimeout(progress_move_offset, (5+progress_offset)*progress_offset);
					}
				}

				let debug;
				const debug_categories = [];
				const categories = ["main", "keyboard", "geometry", "mouse", "clipboard", "draw", "audio", "network"];

				for(let i=0; i<categories.length; i++) {
					const category = categories[i];
					debug = getboolparam("debug_"+category, false);
					if (debug) {
						debug_categories.push(category);
					}
				}
				clog("debug enabled for:", debug_categories);

				// create the client
				const client = new XpraClient('screen');
				client.debug_categories = debug_categories;
				client.remote_logging = remote_logging;
				client.sharing = sharing;
				client.insecure = insecure;
				client.clipboard_enabled = clipboard;
				client.printing = printing;
				client.file_transfer = file_transfer;
				client.bandwidth_limit = bandwidth_limit;
				client.steal = steal;
				client.reconnect = reconnect;
				client.swap_keys = swap_keys;
				client.on_connection_progress = connection_progress;
				client.scroll_reverse_x = scroll_reverse_x;
				client.scroll_reverse_y = scroll_reverse_y;
				client.vrefresh = vrefresh;
				client.scale = scale;
				//example overrides:
				//client.HELLO_TIMEOUT = 3600000;
				//client.PING_TIMEOUT = 60000;
				//client.PING_GRACE = 30000;
				//client.PING_FREQUENCY = 15000;

				if (debug) {
					//example of client event hooks:
					client.on_open = function() {
						cdebug("network", "connection open");
					};
					client.on_connect = function() {
						cdebug("network", "connection established");
					};
					client.on_first_ui_event = function() {
						cdebug("network", "first ui event");
					};
					client.on_last_window = function() {
						cdebug("network", "last window disappeared");
					};
				}

				// mediasource video
				if(video) {
					client.supported_encodings.push("h264");
					if (JSMpeg && JSMpeg.Renderer && JSMpeg.Decoder) {
						client.supported_encodings.push("mpeg1");
					}
					if(mediasource_video) {
						client.supported_encodings.push("vp8+webm", "h264+mp4", "mpeg4+mp4");
					}
				}
				else if(encoding && (encoding !== "auto")) {
					// the primary encoding can be set
					client.enable_encoding(encoding);
				}
				// encodings can be disabled like so
				// client.disable_encoding("h264");
				if(action && (action!="connect")) {
					const sns = {
							"mode" 	: action,
					};
					if (exit_with_children) {
						sns["exit-with-children"] = true;
						if(start) {
							sns["start-child"] = [start];
						}
					}
					else {
						if(start) {
							sns["start"] = [start];
						}
					}
					if (exit_with_client) {
						sns["exit-with-client"] = true;
					}
					client.start_new_session = sns
				}

				// sound support
				if(sound) {
					client.audio_enabled = true;
					clog("sound enabled, audio codec string: "+audio_codec);
					if(audio_codec && audio_codec.includes(":")) {
						const acparts = audio_codec.split(":");
						client.audio_framework = acparts[0];
						client.audio_codec = acparts[1];
					}
					client.audio_mediasource_enabled = getboolparam("mediasource", true);
					client.audio_aurora_enabled = getboolparam("aurora", true);
					client.audio_httpstream_enabled = getboolparam("http-stream", true);
				}

				if(keyboard_layout) {
					client.keyboard_layout = keyboard_layout;
				}

				// check for username and password
				if(username) {
					client.username = username;
				}
				if(password) {
					client.password = password;
				}
				if (action=="connect") {
					client.server_display = display;
				}
				else if (action=="shadow") {
					client.server_display = shadow_display;
				}

				// check for encryption parameters
				if(encryption && encryption.toLowerCase()!="no" && encryption.toLowerCase()!="false") {
					client.encryption = "AES";
					if(key) {
						client.encryption_key = key;
					}
				}

				// attach a callback for when client closes
				const debug_main = getboolparam("debug_main", false);
				if(!debug_main) {
					client.callback_close = function(reason) {
						clog("closing: ", reason);
						if(submit) {
							let message = "Connection closed (socket closed)";
							if(reason) {
								message = reason;
							}
							let url = "./connect.html";
							const has_session_storage = Utilities.hasSessionStorage();
							function add_prop(prop, value) {
								if (has_session_storage) {
									if (value===null || value==="undefined") {
										sessionStorage.removeItem(prop);
									}
									else {
										sessionStorage.setItem(prop, value);
									}
								} else {
									if (value===null || value==="undefined") {
										value = "";
									}
									url = url + "&"+prop+"="+encodeURIComponent(""+value);
								}
							}
							add_prop("disconnect", message);
							const props = {
									"username"			: username,
									"insecure"			: insecure,
									"server"			: server,
									"port"				: port,
									"ssl"				: ssl,
									"path"				: path,
									"encryption"		: encryption,
									//"key"				: key,
									"encoding"			: encoding,
									"bandwidth_limit"	: bandwidth_limit,
									"keyboard_layout"	: keyboard_layout,
									"swap_keys"			: swap_keys,
									"scroll_reverse_x"	: scroll_reverse_x,
									"scroll_reverse_y"	: scroll_reverse_y,
									"reconnect"			: reconnect,
									"action"			: action,
									"display"			: display,
									"shadow_display"	: shadow_display,
									"start"				: start,
									"sound"				: sound,
									"audio_codec"		: audio_codec,
									"keyboard"			: keyboard,
									"clipboard"			: clipboard,
									"printing"			: printing,
									"file_transfer"		: file_transfer,
									"exit_with_children": exit_with_children,
									"exit_with_client"	: exit_with_client,
									"sharing"			: sharing,
									"steal"				: steal,
									"video"				: video,
									"mediasource_video"	: mediasource_video,
									"remote_logging"	: remote_logging,
									"ignore_audio_blacklist" : ignore_audio_blacklist,
									"floating_menu"		: floating_menu,
									"toolbar_position"	: toolbar_position,
									"autohide"			: autohide,
									};
							for (let i=0; i<client.debug_categories.length; i++) {
								const category = client.debug_categories[i];
								props["debug_"+category] = true;
							}
							if (insecure || Utilities.hasSessionStorage()) {
								props["password"] = password;
							}
							else {
								props["password"] = "";
							}
							for (let name in props) {
								const value = props[name];
								add_prop(name, value);
							}
							window.location=url;
						} else {
							// if we didn't submit through the form, silently redirect to the connect gui
							window.location="connect.html";
						}
					}
				}
				client.init(ignore_audio_blacklist);

				client.host = server;
				client.port = port;
				client.ssl = ssl;
				client.path = path.split("index.html")[0];
				return client;
			}

			function init_tablet_input(client) {
			   	//keyboard input for tablets:
				const pasteboard = $('#pasteboard');
				pasteboard.on("input", function(e) {
					const txt = pasteboard.val();
					pasteboard.val("");
			   		cdebug("keyboard", "oninput: '"+txt+"'");
			   		if (!client.topwindow) {
			   			return;
			   		}
					for (let i = 0; i < txt.length; i++) {
						const str = txt[i];
						const keycode = str.charCodeAt(0);
						let keyname = str;
						if (str in CHAR_TO_NAME) {
							keyname = CHAR_TO_NAME[str];
							if (keyname.includes("_")) {
								//ie: Thai_dochada
								const lang = keyname.split("_")[0];
								const key_language = KEYSYM_TO_LAYOUT[lang];
								client._check_browser_language(key_language);
							}
						}
				   		try {
				   			const modifiers = [];
				   			const keyval = keycode;
				   			const group = 0;
							let packet = ["key-action", client.topwindow, keyname, true, modifiers, keyval, str, keycode, group];
							cdebug("keyboard", packet);
							client.send(packet);
							packet = ["key-action", client.topwindow, keyname, false, modifiers, keyval, str, keycode, group];
							cdebug("keyboard", packet);
							client.send(packet);
				   		}
				   		catch (e) {
					   		client.exc(e, "input handling error");
				   		}
			   		}
			   	});
			}

			function init_clipboard(client) {
				const pasteboard = $('#pasteboard');
				//clipboard hooks:
				pasteboard.on('paste', function (e) {
					let paste_data;
					if (navigator.clipboard && navigator.clipboard.readText) {
						navigator.clipboard.readText().then(function(text) {
							cdebug("clipboard", "paste event, text=", text);
							const paste_data = unescape(encodeURIComponent(text));
							client.clipboard_buffer = paste_data;
							client.send_clipboard_token(paste_data);
						}, function(err) {
							cdebug("clipboard", "paste event failed:", err);
						});
					}
					else {
						let datatype = "text/plain";
						let clipboardData = (e.originalEvent || e).clipboardData;
						//IE: must use window.clipboardData because the event clipboardData is null!
						if (!clipboardData) {
							clipboardData = window.clipboardData;
						}
						if (Utilities.isIE()) {
							datatype = "Text";
						}
						paste_data = unescape(encodeURIComponent(clipboardData.getData(datatype)));
						cdebug("clipboard", "paste event, data=", paste_data);
						client.clipboard_buffer = paste_data;
						client.send_clipboard_token(paste_data);
					}
					return false;
				});
				pasteboard.on('copy', function (e) {
					const clipboard_buffer = client.get_clipboard_buffer();
					pasteboard.text(decodeURIComponent(escape(clipboard_buffer)));
					pasteboard.select();
					cdebug("clipboard", "copy event, clipboard buffer=", clipboard_buffer);
					client.clipboard_pending = false;
					return true;
				});
				pasteboard.on('cut', function (e) {
					const clipboard_buffer = client.get_clipboard_buffer();
					pasteboard.text(decodeURIComponent(escape(clipboard_buffer)));
					pasteboard.select();
					cdebug("clipboard", "cut event, clipboard buffer=", clipboard_buffer);
					client.clipboard_pending = false;
					return true;
				});
				function may_set_clipboard() {
					cdebug("clipboard", "pending=", client.clipboard_pending, "buffer=", client.clipboard_buffer);
					if (!client.clipboard_pending) {
						return;
					}
					let clipboard_buffer = client.get_clipboard_buffer();
					const clipboard_datatype = (client.get_clipboard_datatype() || "").toLowerCase();
					const is_text = clipboard_datatype.indexOf("text")>=0 || clipboard_datatype.indexOf("string")>=0;
					if (!is_text) {
						//maybe just abort here instead?
						clipboard_buffer = "";
					}
					pasteboard.text(clipboard_buffer);
					pasteboard.select();
					cdebug("clipboard", "click event, with pending clipboard datatype=", clipboard_datatype, ", buffer=", clipboard_buffer);
					//for IE:
					let success = false;
					if (window.hasOwnProperty("clipboardData") && window.clipboardData.hasOwnProperty("setData") && typeof window.clipboardData.setData === "function") {
						try {
							if (Utilities.isIE()) {
								window.clipboardData.setData("Text", clipboard_buffer);
							}
							else {
								window.clipboardData.setData(clipboard_datatype, clipboard_buffer);
							}
							success = true;
						}
						catch (e) {
							success = false;
						}
					}
					if (!success && is_text) {
						success = document.execCommand('copy');
					}
					else {
						//probably no point in trying again?
					}
					if (success) {
						//clipboard_buffer may have been cleared if not set to text:
						client.clipboard_buffer = clipboard_buffer;
						client.clipboard_pending = false;
					}
				}
				$('#screen').on('click', function (e) {
					may_set_clipboard();
				});
				$("#screen").keypress(function() {
  					may_set_clipboard();
				});
			}

			function init_keyboard(client) {
				const Keyboard = window.SimpleKeyboard.default;
				let kb = new Keyboard({
					onKeyPress: button => onKeyPress(button),
					onKeyReleased: button => onKeyReleased(button),
					display : {
						".com" 	: "|",
						"{tab}" : "tab",
						"{lock}" : "lock",
						"{shift}" : "shift",
						"{bksp}" : "bksp",
						"{space}" : "space",
						"{enter}" : "return",
					},
				});
				function onChange(input){
					clog("Input changed", input);
				}
				function onKeyPress(button){
					forward_key(true, button);
				}
				function onKeyReleased(button){
					forward_key(false, button);
				}
				function forward_key(pressed, button) {
					let key = {
						"{bksp}" 	: "Backspace",
						"{enter}" 	: "Return",
						"{space}" 	: "Space",
						"{tab}" 	: "Tab",
						"{lock}"	: "CapsLock",
						"{shift}"	: "Shift",
						".com"		: "|",
						}[button] || button;
					let e = {
						"which" : 0,
						"keyCode" : 0,
						"key" : key,
						"code" : key,
					};
					client.do_keyb_process(pressed, e);
				}
				const keyboard = getboolparam("keyboard", Utilities.isMobile());
				if (!keyboard) {
					$('.simple-keyboard').hide();
					$('#keyboard_button').css('color', '#777');
				}
				else {
					$('.simple-keyboard').show();
					$('#keyboard_button').css('color', '#111');
				}
			}
			function toggle_keyboard() {
				$('.simple-keyboard').toggle();
				if ($('.simple-keyboard').is(":visible")) {
					$('#keyboard_button').css('color', '#111');
				}
				else {
					$('#keyboard_button').css('color', '#777');
				}
			}

			function init_file_transfer(client) {
				function send_file(f) {
					clog("file:", f.name, ", type:", f.type, ", size:", f.size);
					const fileReader = new FileReader();
					fileReader.onloadend = function (evt) {
						const u8a = new Uint8Array(evt.target.result);
						const buf = Utilities.Uint8ToString(u8a);
						client.send_file(f.name, f.type, f.size, buf);
					};
					fileReader.readAsArrayBuffer(f);
				}
				function sendAllFiles(files) {
					for (let i = 0, f; f = files[i]; i++) {
						send_file(f);
					}
				}
				function handleFileSelect(evt) {
					evt.stopPropagation();
					evt.preventDefault();
					sendAllFiles(evt.dataTransfer.files);
				}
				function handleDragOver(evt) {
					evt.stopPropagation();
					evt.preventDefault();
					evt.dataTransfer.dropEffect = 'copy';
				}
				const screen = document.getElementById('screen');
				screen.addEventListener('dragover', handleDragOver, false);
				screen.addEventListener('drop', handleFileSelect, false);

				$("#upload").on("change", function(evt) {
					evt.stopPropagation();
					evt.preventDefault();
					sendAllFiles(this.files);
					return false;
				});
				$("#upload_form").on('submit',function(evt){
					evt.preventDefault();
				});
			}

			function init_clock(client, enabled) {
				if (!enabled) {
					$("#clock_menu_entry").hide();
					return;
				}
				function update_clock() {
					const now = new Date().getTime();
					const server_time = client.last_ping_server_time + (now - client.last_ping_local_time)+client.server_ping_latency;
					const date = new Date(server_time);
					const clock_text = $("#clock_text");
					const width = clock_text.width();
					clock_text.text(date.toLocaleDateString()+" "+date.toLocaleTimeString());
					const clock_menu_text = $("#clock_menu_text");
					clock_menu_text.text(date.toLocaleDateString()+" "+date.toLocaleTimeString());
					if (width != clock_text.width()) {
						//trays have been shifted left or right:
						client.reconfigure_all_trays();
					}
					//try to land at the half-second point,
					//so that we never miss displaying a second:
					let delay = (1500 - (server_time % 1000)) % 1000;
					if (delay<10) {
						delay = 1000;
					}
					setTimeout(update_clock, delay);
				}

				function wait_for_time() {
					if (client.last_ping_local_time>0) {
						update_clock();
					}
					else {
						//check again soon:
						//(ideally, we should register a callback on the ping packet)
						setTimeout(wait_for_time, 1000);
					}
				}
				wait_for_time();
			}

			function init_audio(client) {
				client.on_audio_state_change = function(newstate, details) {
					if (client.audio_state == newstate) {
						return;
					}
					client.audio_state = newstate;
					let tooltip;
					let data_icon;
					if (newstate=="playing") {
						data_icon = "volume_up";
						tooltip = "audio playing,\nclick to stop";
					}
					else if (newstate=="waiting") {
						data_icon = "volume_up";
						tooltip = "audio buffering";
					}
					else {
						data_icon = "volume_off";
						tooltip = "audio off,\nclick to start";
					}
					clog("audio-state:", newstate);
					const sound_button_element = $("#sound_button");
					sound_button_element.attr("data-icon", data_icon);
					sound_button_element.attr("title", tooltip);
				};
				if (client.audio_enabled) {
					$("#sound_button").click(function() {
						clog("speaker icon clicked, audio_enabled=", client.audio_enabled);
						if (client.audio_state=="playing" || client.audio_state=="waiting") {
							client.on_audio_state_change("stopped", "user action");
							client.close_audio();
						}
						else {
							client.on_audio_state_change("waiting", "user action");
							client._audio_start_stream();
							client._sound_start_receiving();
						}
					})
				}
				else {
					$("#sound_button").hide();
				}
			}

			function toggle_fullscreen() {
				const f_el = document.hasOwnProperty("requestFullScreen") || document.hasOwnProperty("webkitRequestFullScreen") || document.hasOwnProperty("mozRequestFullScreen") || document.hasOwnProperty("msRequestFullscreen");
				if (!f_el) {
					const elem = document.getElementById("fullscreen_button");
					var is_fullscreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
					if (!is_fullscreen) {
						const req = elem.requestFullScreen || elem.webkitRequestFullScreen || elem.mozRequestFullScreen || elem.msRequestFullscreen;
						if (req) {
							req.call(document.body);
							$('#fullscreen').attr('src', './icons/unfullscreen.png');
							elem.title = "Exit Fullscreen";
							$('#fullscreen_button').attr('data-icon', 'fullscreen_exit');
						}
					} else {
						if (document.exitFullscreen) {
							document.exitFullscreen();
						} else if (document.webkitExitFullscreen) {
							document.webkitExitFullscreen();
						} else if (document.hasOwnProperty("mozCancelFullScreen")) {
							document.mozCancelFullScreen();
						} else if (document.hasOwnProperty("msExitFullscreen")) {
							document.msExitFullscreen();
						}

						$('#fullscreen').attr('src', './icons/fullscreen.png');
						elem.title = "Fullscreen";
						$('#fullscreen_button').attr('data-icon', 'fullscreen');
					}
				}
			}

			function expand_float_menu(){
				const expanded_width = (float_menu_item_size*5);
				document.getElementById('float_menu').style.width = expanded_width +'px';
				$('#float_menu').children().show();
				if(client){
					client.reconfigure_all_trays();
				}
			}
			
			function retract_float_menu(){
				document.getElementById('float_menu').style.width ='0px'; $('#float_menu').children().hide();
			}

			let client;
			function init_page() {
				const floating_menu = getboolparam("floating_menu", true);
				const autohide = getboolparam("autohide", false);
				const toolbar_position = getparam("toolbar_position");
				const float_menu_element = $('#float_menu');
				if (!floating_menu) {
					float_menu_element.hide();
				}
				else {
					var toolbar_width = float_menu_element.width();
					var left = 0;
					var top = float_menu_element.offset().top || 0;
					var screen_width = $('#screen').width();
					if (toolbar_position=="top-left") {
						//no calculations needed
					}
					else if (toolbar_position=="top") {
						left = screen_width/2-toolbar_width/2;
					}
					else if (toolbar_position=="top-right") {
						left = screen_width-toolbar_width-100;
					}
					float_menu_element.offset({ top: top, left: left });

					if (autohide) {
						float_menu_element.on('mouseover', expand_float_menu);
						float_menu_element.on('mouseout', retract_float_menu);
					}
					else {
						expand_float_menu();
					}
				}

				const touchaction = getparam("touchaction") || "scroll";
				touchaction_scroll = touchaction == "scroll";
				set_touchaction();
				
				client = init_client();
				client.connect();

				//from now on, send log and debug to client function
				//which may forward it to the server once connected:
				clog = function() {
					client.log.apply(client, arguments);
				};
				cdebug = function() {
					client.debug.apply(client, arguments);
				};
				init_tablet_input(client);
				if (client.clipboard_enabled) {
					init_clipboard(client);
				}
			   	if (client.file_transfer) {
			   		init_file_transfer(client);
			   	}
			   	else {
			   		$('upload_link').removeAttr('href');
			   	}
				init_keyboard(client);
				init_clock(client, getboolparam("clock", true));
				init_audio(client);
				document.addEventListener("visibilitychange", function (e) {
					const window_ids = Object.keys(client.id_to_window).map(Number);
					clog("visibilitychange hidden=", document.hidden, "connected=", client.connected);
					if (client.connected) {
						if (document.hidden) {
							client.send(["suspend", true, window_ids]);
						}
						else {
							client.send(["resume", true, window_ids]);
							client.redraw_windows();
							client.request_refresh(-1);
						}
					}
				});
				$('#fullscreen').on('click', function (e) {
					toggle_fullscreen();
				});

				$('#fullscreen_button').on('click', function (e) {
					toggle_fullscreen();
				});

				$('#keyboard_button').on('click', function (e) {
					toggle_keyboard();
				});

				const screen_change_events = "webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange";
				$(document).on(screen_change_events, function () {
					const f_el = document.fullscreenElement || document.mozFullScreen || document.webkitIsFullScreen || document.msFullscreenElement;
					clog("fullscreenchange:", f_el);
					if (f_el == null) {
						//we're not in fullscreen mode now, so show fullscreen icon again:
						$('#fullscreen_button').attr('data-icon', 'fullscreen');
					}
				});
				// disable right click menu:
				window.oncontextmenu = function(e) {
					client._poll_clipboard(e);
					return false;
				}
			}

			$(document).ready(function() {
				clog("document is ready, browser is", navigator.platform, "64-bit:", Utilities.is_64bit());
				const xhr = new XMLHttpRequest();
				xhr.open("GET", "./default-settings.txt");
				xhr.onload = function() {
					const tmp_default_settings = Utilities.parseINIString(xhr.responseText);
					clog("network", "got default settings:", tmp_default_settings);
					//update the default settings object
					//so getparam will look there
					for (const key in tmp_default_settings) {
						default_settings[key] = tmp_default_settings[key];
					}
					init_page();
				};
				xhr.onerror  = function() {
					clog("network", "default settings request failed with code", xhr.status);
					init_page();
				};
				xhr.onabort = function() {
					clog("network", "default settings request aborted");
					init_page();
				};
				xhr.send();
			});
		</script>
	</body>
</html>
